package algorithms;
import util.DoubleVector;
import weka.classifiers.Classifier;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Utils;

public class AWE extends weka.classifiers.Classifier{

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	private static final class ClassifierWeightComparator implements
			java.util.Comparator<double[]> {
		@Override
		public int compare(double[] o1, double[] o2) {
			if (o1[0] > o2[0])
				return 1;
			else if (o1[0] < o2[0])
				return -1;
			else
				return 0;
		}
	}

//	private double trainingWeightSeenByModel = 0.0; // moa abstract classifier
			
	protected static java.util.Comparator<double[]> weightComparator = new ClassifierWeightComparator();

	protected long[] classDistributions;
	protected Classifier[] pool = new Classifier[0];
	protected double[][] storedWeights = new double[5][2];
	protected Classifier[] ensemble;
	protected double[] ensembleWeights;
	private int ensembleSize = 0;
	private int maxEnsembleSize = 10;
	
	protected int maxNumberOfLearners = 20;
	protected int folds = 6; // number of folds in cross validation
	
	protected Classifier baseLearner;
	protected Instances[] batchs;
	protected Instances chunk = null;
	protected int chunkSize = 0;
	protected int chunk_pointer = 0;
	
	public AWE(Instances[] chunks, Classifier learner){
		this.baseLearner = learner;
		this.batchs = chunks;
		init();
	}
	
	public void setLearnModel(Classifier cls){
		this.baseLearner = cls;
	}
	
	public void setNumberOfFolds(int folds){
		this.folds = folds;
	}
	
	public void setMaxNumberOfLearners(int maxNum){
		this.maxNumberOfLearners = maxNum;
	}
	
	public void setMaxEnsembleSize(int max){
		this.maxEnsembleSize = max;
	}
	
	public void reset() throws Exception{
		Classifier t = Classifier.makeCopy(this.baseLearner);
		this.baseLearner = t;
	}
	
	private void init() {
		if (this.chunk == null) {
			this.chunk = batchs[0];
		}
		
		this.chunkSize = this.chunk.numInstances(); 

		if (this.classDistributions == null) {
			this.classDistributions = new long[chunk.classAttribute().numValues()];

//	DEBUG		for (int i = 0; i < this.classDistributions.length; i++) {
//	DEBUG			this.classDistributions[i] = 0;
//	DEBUG		}
		
			for(int i=0; i < chunkSize; ++i){
				this.classDistributions[(int) chunk.instance(i).classValue()]++;
			}
			
		}
	}
	
	public boolean nextChunk(){
		chunk_pointer++;
		if(chunk_pointer < batchs.length){
			this.chunk = batchs[chunk_pointer];
			init();
			return true;
		}else {
			chunk_pointer--;
			this.chunk = batchs[chunk_pointer];
			init();
			return false;
		}
	}
	
	public void process() throws Exception{
		do{
			processChunk();
			System.out.println("A chunk of data has been processed");
		}while(nextChunk());
	}	
	
	/*
	 *   process(){
	 *   
	 *    weight <- cmputeCandidateWeight(this.basedLearner, currentChunk);
	 *    
	 *    each candidate for pool
	 *      candidate->weight = computeWeight(candidate,currentChunk); 
	 *     
	 *    if(!pool.isFull) 
	 *       pool.add(weigth,basedLearner)
	 *       
	 *    else   
	 *       replace the candidate with lowest weight
	 *   }
	 * 
	 *   use the k candidate with highest weight to make an ensemble 
	 * 
	 */
	
	protected void processChunk() throws Exception {
		// Compute weights
		double candidateClassifierWeight = this.computeCandidateWeight(
				this.baseLearner, this.chunk, this.folds);
	//DEBUG	System.out.println("candidate weight "+candidateClassifierWeight); // DEBUG

			for (int i = 0; i < this.pool.length; i++) {
	//DEBUG			System.out.println("classifier "+i+" "+this.computeWeight(
	//DEBUG					this.pool[i], this.chunk)); 
				this.storedWeights[i][0] = this.computeWeight(
					this.pool[i], this.chunk);
			}

		if (this.pool.length < this.maxNumberOfLearners) {
			// Train and add classifier
			this.baseLearner.buildClassifier(chunk);
			this.addToPool(this.baseLearner, candidateClassifierWeight);
		} else {
			// Substitute poorest classifier
			java.util.Arrays.sort(this.storedWeights, weightComparator);

			if (this.storedWeights[0][0] < candidateClassifierWeight) {
//				for (int num = 0; num < this.chunkSize; num++) {
//					this.baseLearner.trainOnInstance(this.chunk
//							.instance(num));
//				}
				this.baseLearner.buildClassifier(chunk);
				this.storedWeights[0][0] = candidateClassifierWeight;
				this.pool[(int) this.storedWeights[0][1]] = Classifier.makeCopy(this
						.baseLearner);
			}
		}
		
		//this.ensembleSize=java.lang.Math.min(this.pool.length,this.maxNumberOfLearners);
		this.ensembleSize=java.lang.Math.min(this.pool.length, this.maxEnsembleSize);
		this.ensemble = new Classifier[ensembleSize];
		this.ensembleWeights = new double[ensembleSize];

		// Select top k classifiers to construct the ensemble
//		for (int i = 0; i < ensembleSize; i++) {
//			this.ensembleWeights[i] = this.storedWeights[ensembleSize - i - 1][0];
//			this.ensemble[i] = this.pool[(int) this.storedWeights[ensembleSize
//					- i - 1][1]];
//		}
		this.createEnsemble();

		this.classDistributions = null;
		this.chunk = null;
//		this.candidateClassifier = (Classifier) Class.forName("LibSVM").newInstance();
//		this.candidateClassifier.resetLearning();
		this.reset(); // Renew the base learner
	}
	
	private void createEnsemble(){
		java.util.Arrays.sort(this.storedWeights, weightComparator);
		for (int i = 0; i < this.ensembleSize; i++) {
			this.ensembleWeights[i] = this.storedWeights[this.storedWeights.length - i -1][0];
			this.ensemble[i] = this.pool[(int) this.storedWeights[this.storedWeights.length
					- i - 1][1]];
		}
	}

	protected double computeCandidateWeight(Classifier candidate,
			Instances chunk, int numFolds) throws Exception{
		double candidateWeight = 0.0;
		java.util.Random random = new java.util.Random(1);
		Instances randData = new Instances(chunk);
		randData.randomize(random);
		if (randData.classAttribute().isNominal()) {
			randData.stratify(numFolds);
		}

		for (int n = 0; n < numFolds; n++) {
			Instances train = randData.trainCV(numFolds, n, random);
			Instances test = randData.testCV(numFolds, n);

//			Classifier learner = candidate.copy();
			Classifier learner = Classifier.makeCopy(candidate);

//			for (int num = 0; num < train.numInstances(); num++){
//				  learner.trainOnInstance(train.instance(num));
//		    }
			learner.buildClassifier(train);
			
			candidateWeight += computeWeight(learner, test);
		}
		return candidateWeight / numFolds;
	}

	protected double computeWeight(Classifier learner, Instances chunk) {
		double mse_i = 0;
		double mse_r = 0;

		double f_ci;
		double voteSum;

		for (int i = 0; i < chunk.numInstances(); i++) {
			try {
				voteSum = 0;
				for (double element : learner.distributionForInstance(chunk.instance(i))) {
						voteSum += element;
				}

				if (voteSum > 0) {
				   f_ci = learner.distributionForInstance(chunk.instance(i))[(int) chunk.instance(i).classValue()]
								/ voteSum;
				   mse_i += (1 - f_ci) * (1 - f_ci);
				} else {
				    mse_i += 1;
				  }
				} catch (Exception e) {
					mse_i += 1;
				}
		}

		mse_i /= this.chunk.numInstances();  // chunkSize to POC
		mse_r = this.computeMseR();

//		System.out.println(mse_r - mse_i);
		return java.lang.Math.max(mse_r - mse_i, 0); // 0
	}

	protected double computeMseR() {
		double p_c;
		double mse_r = 0;

		for (int i = 0; i < this.classDistributions.length; i++) {
			p_c = (double) this.classDistributions[i] / (double) this.chunkSize;
			mse_r += p_c * ((1 - p_c) * (1 - p_c));
		}
		return mse_r;
	}

	protected int removePoorestModelBytes() {
		int poorestIndex = Utils.minIndex(this.ensembleWeights);
		// int byteSize = this.ensemble[poorestIndex].measureByteSize();
		discardModel(poorestIndex);
		// return byteSize;
		return 0;
	}
	
	public Classifier[] getSubClassifiers() {
		return this.ensemble.clone();
	}

	protected Classifier addToPool(Classifier newClassifier,
			double newClassifiersWeight) throws Exception {
//		Classifier addedClassifier = null;
		Classifier[] newStored = new Classifier[this.pool.length + 1];
		double[][] newStoredWeights = new double[newStored.length][2];

		for (int i = 0; i < newStored.length; i++) {
			if (i < this.pool.length) {
				newStored[i] = this.pool[i];
				newStoredWeights[i][0] = this.storedWeights[i][0];
				newStoredWeights[i][1] = i;
			} else {
//				newStored[i] = addedClassifier = newClassifier.copy();
//				newStored[i] = addedClassifier = Classifier.makeCopy(newClassifier);
				newStored[i] = newClassifier;
				newStoredWeights[i][0] = newClassifiersWeight;
				newStoredWeights[i][1] = i;
			}
		}
		this.pool = newStored;
		this.storedWeights = newStoredWeights;

		return newClassifier;
	}

	
	
	protected void discardModel(int index) {
		Classifier[] newEnsemble = new Classifier[this.ensemble.length - 1];
		double[] newEnsembleWeights = new double[newEnsemble.length];
		int oldPos = 0;
		for (int i = 0; i < newEnsemble.length; i++) {
			if (oldPos == index) {
				oldPos++;
			}
			newEnsemble[i] = this.ensemble[oldPos];
			newEnsembleWeights[i] = this.ensembleWeights[oldPos];
			oldPos++;
		}
		this.ensemble = newEnsemble;
		this.ensembleWeights = newEnsembleWeights;
	}

	@Override
	public void buildClassifier(Instances dataset) throws Exception {
		this.baseLearner.buildClassifier(dataset);
	}
	
	public double[] distributionForInstance(Instance inst) throws Exception {
		DoubleVector combinedVote = new DoubleVector();

			for (int i = 0; i < this.ensemble.length; i++) {
				if (this.ensembleWeights[i] > 0.0) {
					DoubleVector vote = new DoubleVector(this.ensemble[i]
							.distributionForInstance(inst));

					if (vote.sumOfValues() > 0.0) {
						vote.normalize();
						vote.scaleValues(this.ensembleWeights[i]);
						combinedVote.addValues(vote);
					}
				}
			}

		combinedVote.normalize();
		return combinedVote.getArrayRef();
	}
	
	public double classifyInstance(Instance inst) throws Exception {
//		double max = java.lang.Double.MIN_VALUE;
//		double idx = -1;
//		double[] dis = distributionForInstance(inst);
//		for(int i=0; i < dis.length; ++i){
//			if( dis[i] > max){
//				max = dis[i];
//				idx = i;
//			}
//		}	
//		return idx;
		double[] dis = this.distributionForInstance(inst);
		return weka.core.Utils.maxIndex(dis);
		
	}
	
	public void information(){
		System.out.println(pool.length + " classifiers have been trained");
		System.out.println(ensemble.length + " classifiers in ensemble");
		System.out.println("Weights for each classifier in ensemble");
		for(int i=0; i<ensemble.length; ++i)
			System.out.println("Classifier"+this.storedWeights[
	     this.storedWeights.length-i-1][1]+" : "+this.ensembleWeights[i]);
	}
	
	// return classifiers in pool
	public Classifier getClassifier(int idx){
		return pool[idx];
	}
	
	public boolean addClassifier(Classifier aClassifier) {
	    boolean sccuess = true;
		double aWeight = computeWeight(aClassifier, this.chunk);
		try{
			this.addToPool(aClassifier,aWeight);
			this.ensembleSize=java.lang.Math.min(this.pool.length, this.maxEnsembleSize);
			this.ensemble = new Classifier[ensembleSize];
			this.ensembleWeights = new double[ensembleSize];
			this.createEnsemble();
		}catch(Exception e){
			e.printStackTrace();
			sccuess = false;
		}
		return sccuess;
	}
	
   public boolean addCurrentClassifier(Classifier aClassifier) throws Exception{
	   boolean sccuess = true;
	   double  weight = this.computeCandidateWeight(
				this.baseLearner, this.chunk, this.folds);
	   try{
		  this.addToPool(aClassifier, weight);
		  this.ensembleSize=java.lang.Math.min(this.pool.length, this.maxEnsembleSize);
		  this.ensemble = new Classifier[ensembleSize];
		  this.ensembleWeights = new double[ensembleSize];
		  this.createEnsemble();
		}catch(Exception e){
			e.printStackTrace();
			sccuess = false;
		}
		return sccuess;
   }
	
	
}
